/*
  author Sylvain Bertrand <digital.ragnarok@gmail.com>
  Protected by GNU Affero GPL v3 with some exceptions.
  See README at root of alga tree.
*/

#include <linux/module.h>
#include <linux/slab.h>
#include <asm/io.h>
#include <linux/device.h>
#include <asm/unaligned.h>
#include <linux/mutex.h>

#include <alga/amd/atombios/atb.h>

#include "tables/atb.h"
#include "tables/cmd.h"
#include "tables/data.h"
#include "tables/firmware_info.h"
#include "tables/asic_init.h"
#include "tables/firmware_vram_usage.h"

#include "atb.h"
#include "regs.h"
#include "scratch_pads.h"
#include "interpreter.h"
#include "iio_interpreter.h"

int atb_asic_init(struct atombios *atb)
{
	u16 of;
	struct master_data_tbl *data_tbl;
	struct firmware_info_subset *info;
	struct master_cmd_tbl *cmd_tbl;
	struct common_cmd_tbl_hdr *asic_init;
	struct asic_init_params *ps;
	int r;

	mutex_lock(&atb->mutex);

	/* engine and memory default clocks */
	of = get_unaligned_le16(&atb->hdr->master_data_tbl_of);
	data_tbl = atb->adev.rom + of;

	of = get_unaligned_le16(&data_tbl->list.firmware_info);
	info = atb->adev.rom + of;

	dev_info(atb->adev.dev, "atombios: firmware_info (0x%04x) revision "
					"%u.%u\n", of, info->hdr.tbl_fmt_rev,
						info->hdr.tbl_content_rev);
	dev_info(atb->adev.dev, "atombios:   firmware revision 0x%08x\n",
				get_unaligned_le32(&info->firmware_rev));
	dev_info(atb->adev.dev, "atombios:   default engine clock %ukHz\n",
			get_unaligned_le32(&info->default_eng_clk) * 10);
	dev_info(atb->adev.dev, "atombios:   default memory clock %ukHz\n",
			get_unaligned_le32(&info->default_mem_clk) * 10);

	/* asic_init call */
	of = get_unaligned_le16(&atb->hdr->master_cmd_tbl_of);
	cmd_tbl = atb->adev.rom + of;
	of = get_unaligned_le16(&cmd_tbl->list.asic_init);

	asic_init = atb->adev.rom + of;
	dev_info(atb->adev.dev, "atombios: asic_init (0x%04x) revision %u.%u\n",
		of, asic_init->hdr.tbl_fmt_rev, asic_init->hdr.tbl_content_rev);

	atb->g_ctx.ps_dws = 0x80;/* max for ps index */
	atb->g_ctx.ps_top = kzalloc(atb->g_ctx.ps_dws * 4, GFP_KERNEL);
	if (!atb->g_ctx.ps_top) {
		dev_err(atb->adev.dev, "atombios: unable to allocate parameter"
		" space (stack)\n");
		r = -ATB_ERR;
		goto unlock_mutex;
	}

	ps = (struct asic_init_params *)atb->g_ctx.ps_top;	
	put_unaligned_le32(get_unaligned_le32(&info->default_eng_clk),
							&ps->default_eng_clk);
	put_unaligned_le32(get_unaligned_le32(&info->default_mem_clk),
							&ps->default_mem_clk);

	/* reset some global runtime workspace data */
	atb->g_ctx.fb_wnd = 0;
	atb->g_ctx.regs_blk = 0;
	atb->g_ctx.io_mode = IO_MM;

	r = interpret(atb, of, 0, 0);
	kfree(atb->g_ctx.ps_top);

unlock_mutex:
	mutex_unlock(&atb->mutex);
	return r;
}
EXPORT_SYMBOL_GPL(atb_asic_init);

struct atombios *atb_alloc(struct atb_dev *adev)
{
	struct atombios *atb;

	atb = kzalloc(sizeof(struct atombios), GFP_KERNEL);
	if (!atb)
		return NULL;
	atb->adev = *adev;
	return atb;
}
EXPORT_SYMBOL_GPL(atb_alloc);

static void scratch_regs_setup(struct atombios *atb)
{
	u32 s2;
	u32 s6;

	s2 = atb->adev.rr32(atb->adev.dev, S2);
	s6 = atb->adev.rr32(atb->adev.dev, S6);

	/* let the bios control the backlight */
	s2 &= ~S2_VRI_BRIGHT_ENA;

	/* tell the bios not to handle mode switching */
	s6 |= S6_ACC_BLK_DISP_SWITCH;

	atb->adev.wr32(atb->adev.dev, s2, S2);
	atb->adev.wr32(atb->adev.dev, s6, S6);
}

static int scratch_mem_setup(struct atombios *atb)
{
	struct master_data_tbl *data_tbl;
	struct firmware_vram_usage *usage; /* use rev 1.1 table layout */
	u16 of;
	size_t bytes;
	u64 start_addr;
	u16 sz;
	
	of = get_unaligned_le16(&atb->hdr->master_data_tbl_of);
	data_tbl = atb->adev.rom + of;

	of = get_unaligned_le16(&data_tbl->list.firmware_vram_usage);
	usage = atb->adev.rom + of;

	start_addr = (u64)get_unaligned_le32(&usage->info.start_addr);
	if (usage->hdr.tbl_content_rev >= 4)
		start_addr = start_addr * 1024;
	sz = get_unaligned_le16(&usage->info.sz);

	/*
	 * This table defines a large data buffer for the interpreter.
	 * It's actually in kernel RAM. The "vram" name comes from the
	 * fact that this large data buffer is in vram when running
	 * in POST real mode.
	 */
	dev_info(atb->adev.dev, "atombios: firmware_(v)ram_usage (0x%04x) "
				"revision %u.%u\n", of, usage->hdr.tbl_fmt_rev,
						usage->hdr.tbl_content_rev);
	dev_info(atb->adev.dev, "atombios: firmware_(v)ram_usage address is"
						" 0x%016llx\n", start_addr);
	dev_info(atb->adev.dev, "atombios: firmware_(v)ram_usage size is "
							"%zukB\n", (size_t)sz);

	if (sz != 0)
		bytes = sz * 1024;
	else
		bytes = 20 * 1024; /* quirk: get 20kB if zero or not defined */

	atb->scratch = kzalloc(bytes, GFP_KERNEL);
	if (!atb->scratch) {
		dev_err(atb->adev.dev, "atombios: unable to allocate scratch "
							"kernel memory\n");
		return -ATB_ERR;
	}
	atb->scratch_sz = bytes;
	dev_info(atb->adev.dev, "atombios: %zuB allocated for scratch "
							"memory\n", bytes);
	return 0;
}

static void atb_name_build(struct atombios *atb, u8 *str)
{
	int i;

	while (*str && ((*str == '\n') || (*str == '\r')))
		str++;
	/* name string isn't always 0 terminated */
	for (i = 0; i < 511; ++i) {
		atb->name[i] = str[i];
		if (atb->name[i] < '.' || atb->name[i] > 'z') {
			atb->name[i] = 0;
			break;
		}
	}
}

int atb_init(struct atombios *atb)
{
	int err;
	u16 hdr_of;
	__le16 *hdr_of_ptr;
	u8 *bootup_msg;

	hdr_of_ptr = (__le16*)(atb->adev.rom + 0x48);
	hdr_of = get_unaligned_le16(hdr_of_ptr);
	atb->hdr = (struct rom_hdr*)(atb->adev.rom + hdr_of); 
	dev_info(atb->adev.dev, "atombios: rom (0x%04x) revision %u.%u\n",
					hdr_of, atb->hdr->hdr.tbl_fmt_rev,
						atb->hdr->hdr.tbl_content_rev);

	if (memcmp(atb->hdr->firmware_signature, "ATOM", 4)) {
		dev_err(atb->adev.dev, "atombios: wrong signature");
		return -ATB_ERR;
	}

	if (memcmp(atb->adev.rom + 0x30, " 761295520", 10)) {
		dev_err(atb->adev.dev, "atombios: wrong AMD signature");
		return -ATB_ERR;
	}

	scratch_regs_setup(atb);
	err = scratch_mem_setup(atb);
	if (err)
		return err;

	err = iio_setup(atb);
	if (err)
		goto free_scratch_mem;
	
	bootup_msg = atb->adev.rom + get_unaligned_le16(
						&atb->hdr->bios_bootup_msg_of);
	atb_name_build(atb, bootup_msg);
	dev_info(atb->adev.dev, "atombios: %s\n", atb->name);
	mutex_init(&atb->mutex);
	return 0;

free_scratch_mem:
	kfree(atb->scratch);
	return err;
}
EXPORT_SYMBOL_GPL(atb_init);

void atb_cleanup(struct atombios *atb)
{
	if (atb->scratch)
		kfree(atb->scratch);
	kfree(atb->iio);
}
EXPORT_SYMBOL_GPL(atb_cleanup);

static int __init init(void)
{
	return 0;
}

static void __exit cleanup(void)
{
}

module_init(init);
module_exit(cleanup);

MODULE_AUTHOR("Sylvain Bertrand <digital.ragnarok@gmail.com>");
MODULE_DESCRIPTION("AMD atombios");
MODULE_LICENSE("GPL");
